package edu.neu.cs6515.turn;

import java.util.LinkedList;
import java.util.List;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.java.contract.Ensures;
import com.google.java.contract.Requires;

import edu.neu.cs6515.admin.Administrator;
import edu.neu.cs6515.board.HexagonBoard;
import edu.neu.cs6515.board.ScoreCalc;
import edu.neu.cs6515.xml.IngeniousColor;
import edu.neu.cs6515.xml.Placement;
import edu.neu.cs6515.xml.Score;
import edu.neu.cs6515.xml.Tile;

public abstract class AbstractTurnProxy implements ITurnProxy {

    /**
     * Bad Reasons
     */
    public static final String TURN_NOT_FINISHED = "PLAYER DID NOT FINISH TURN";
    public static final String TILE_NOT_IN_HAND = "PLAYER TRIES TO USE TILE THAT IS NOT IN PLAYERS HAND";
    public static final String NOT_SUPPOSED_TO_MAKE_MOVE = "NOT SUPPOSED TO MAKE MOVE";
    protected final HexagonBoard board;
    protected List<Tile> tilesInPlayersHand;
    protected Score playersScore;
    private int requiredPlacements = 1;
    protected boolean reRacked = false;
    private List<Placement> placementsInThisTurn = new LinkedList<Placement>();

    protected AbstractTurnProxy(final HexagonBoard board,
            final List<Tile> tilesInPlayersHand, final Score score) {
        super();
        this.board = board;
        this.tilesInPlayersHand = tilesInPlayersHand;
        playersScore = score;
    }

    @Requires({ "placement != null",
            "getErrorsInPlacement(placement).isEmpty()" })
    @Ensures({ "result != null", "result.size() == 0 || result.size() == 1" })
    @Override
    public List<Tile> placeTile(final Placement placement) {
        updateScore(placement);
        board.updateHexagonBoard(placement);
        this.placementsInThisTurn.add(placement);
        removeTileFromPlayersHand(placement);
        updateRequiredPlacements();
        return getNewTile();
    }

    protected abstract List<Tile> getNewTile();

    /**
     * If no placements are possible the required number is set to zero,
     * decreased otherwise
     */
    private void updateRequiredPlacements() {
        if (!board.spotForLegalTilePlacementAvailable()) {
            requiredPlacements = 0;
        } else if (tilesInPlayersHand.isEmpty()) {
            requiredPlacements = 0;
        } else {
            requiredPlacements--;
        }
    }

    @Override
    public List<String> getErrorsInPlacement(final Placement placement) {
        if (requiredPlacements == 0) {
            return ImmutableList.of(NOT_SUPPOSED_TO_MAKE_MOVE);
        }
        if (!isTileInPlayersHand(placement.getC0(), placement.getC1())) {
            return ImmutableList.of(TILE_NOT_IN_HAND);
        }
        if (board.getBrokenRules(placement).size() > 0) {
            return ImmutableList.of(board.getBrokenRules(placement).get(0)
                    .getReason());
        }
        return ImmutableList.of();
    }

    /**
     * Removes the tile from the players hand that the player used in the given
     * placement
     * 
     * @param p
     *            placement by the player
     */
    private void removeTileFromPlayersHand(final Placement p) {
        final Tile tilePlaced = new Tile(p.getC0(), p.getC1());
        for (final Tile tile : tilesInPlayersHand) {
            if (tilePlaced.tileSameColorsAs(tile)) {
                tilesInPlayersHand.remove(tile);
                break;
            }
        }
    }

    /**
     * Updates the score for all colors after a valid placement of a player
     * 
     * @param placement
     *            placement made by a player
     */
    private void updateScore(final Placement placement) {
        final Score scoreOfMove = board.scoreAMove(placement);
        countAdditionalRequiredMoves(scoreOfMove);
        playersScore = board.scoreAMove(placement, playersScore);
    }

    /**
     * Checks each color if it crossed the bonus round and increases the
     * required number of moves
     * 
     * @param scoreOfMove
     *            Score achieved by a move
     */
    private void countAdditionalRequiredMoves(final Score scoreOfMove) {
        increaseForColor(playersScore.getOrange(), scoreOfMove.getOrange());
        increaseForColor(playersScore.getBlue(), scoreOfMove.getBlue());
        increaseForColor(playersScore.getGreen(), scoreOfMove.getGreen());
        increaseForColor(playersScore.getRed(), scoreOfMove.getRed());
        increaseForColor(playersScore.getYellow(), scoreOfMove.getYellow());
        increaseForColor(playersScore.getPurple(), scoreOfMove.getPurple());
    }

    /**
     * Increases the number of placements if a score crosses the bonus limit
     * 
     * @param colorScoreBefore
     *            score of a color before the move
     * @param colorScoreOfMove
     *            points made by the move
     */
    private void increaseForColor(final int colorScoreBefore,
            final int colorScoreOfMove) {
        if (isCrossingBonusLine(colorScoreBefore, colorScoreOfMove)) {
            requiredPlacements++;
        }
    }

    /**
     * @param scoreBeforePlacement
     *            old score
     * @param scoreForPlacement
     *            points made by this placement
     * @return true if a placement brought the score over the bonus round limit
     */
    private boolean isCrossingBonusLine(final int scoreBeforePlacement,
            final int scoreForPlacement) {
        return (scoreBeforePlacement < ScoreCalc.BONUS_SCORE_LINE)
                && (scoreBeforePlacement + scoreForPlacement >=
                    ScoreCalc.BONUS_SCORE_LINE);
    }

    /**
     * @param color1
     *            col1
     * @param color2
     *            col2
     * @return true iff the player holds a tile with the same two colors in his
     *         hand
     */
    private boolean isTileInPlayersHand(final IngeniousColor color1,
            final IngeniousColor color2) {
        for (final Tile tile : tilesInPlayersHand) {
            if (tile.tileSameColorsAs(new Tile(color1, color2))) {
                return true;
            }
        }
        return false;
    }

    @Override
    public List<Tile> getAllTilesInPlayersHand() {
        LinkedList<Tile> copyOfTiles = Lists.newLinkedList();
        for (Tile tile : tilesInPlayersHand) {
            copyOfTiles.add(new Tile(tile.getC0(), tile.getC1()));
        }
        return copyOfTiles;
    }

    @Override
    public Score getCurrentScore() {
        return (Score) playersScore.clone();
    }

    @Override
    public int getRequiredPlacements() {
        return requiredPlacements;
    }

    @Override
    public List<Placement> getAllPlacementsOnBoard() {
        return board.getAllPlacementsOnBoard();
    }

    @Override
    public List<Placement> getPlacementsInThisTurn() {
        return new LinkedList<Placement>(this.placementsInThisTurn);
    }

    /**
     * 
     * @param color
     *            color to be checked
     * @return true if at least one tile has the color
     */
    private boolean isColorInHand(final IngeniousColor color) {
        this.getCurrentScore().getIncreasingOrderSortedScore().get(0);
        for (Tile tile : this.tilesInPlayersHand) {
            if ((tile.getC0().equals(color) || tile.getC1().equals(color))) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return true if at least one tile in hand has any lowest color
     */
    protected boolean isAnyLowestScoredColorInHand() {
        for (IngeniousColor color : this.getCurrentScore()
                .getLowestScoredColors()) {
            if (isColorInHand(color)) {
                return true;
            }
        }
        return false;
    }

    protected boolean isReRacked() {
        return this.reRacked;
    }

}